﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ESRI.ArcGIS.AnalysisTools;
using ESRI.ArcGIS.ConversionTools;
using ESRI.ArcGIS.DataSourcesGDB;
using ESRI.ArcGIS.DataSourcesRaster;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geoprocessing;
using ESRI.ArcGIS.Geoprocessor;
using ESRI.ArcGIS.SpatialAnalystTools;
using ESRI.ArcGIS.DataManagementTools;

namespace Bes.Swsp.ToolBox
{
  public class PolylineRasterIntersect
  {
    private IFeatureClass _lineClass;
    private string _idFieldName = "Id";
    private IRaster _raster;
    private string _rasterFieldName = "Value";
    private string _outFieldPrefix = "Raster";

    private string _dominantFieldName = "Dominant";

    public const int MAX_FIELDS = 20;
    private const string GRIDCODE_FIELD = "gridcode";

    private bool _useGeoprocessor = true;

    public PolylineRasterIntersect(bool useGeoprocessor = true)
    {
      _useGeoprocessor = useGeoprocessor;
    }

    public IFeatureClass PolylineClass
    {
      get { return _lineClass; }
      set { _lineClass = value; }
    }

    public string PolylineIdField
    {
      get { return _idFieldName; }
      set { _idFieldName = value; }
    }

    public IRaster CategoryRaster
    {
      get { return _raster; }
      set { _raster = value; }
    }

    public string RasterField
    {
      get { return _rasterFieldName; }
      set { _rasterFieldName = value; }
    }

    public string OutputFieldPrefix 
    {
      get { return _outFieldPrefix; }
      set { _outFieldPrefix = value; }
    }

    public event EventHandler<ProgressEventArgs> Progress;

    public void Intersect()
    {
      _LogIntersectStart();

      ITable tabulation = null;
      Geoprocessor gp = GeoprocessingUtility.GetGeoprocessor(true, false, true, _raster);
      try
      {
        IDataset lineDataset = (IDataset)_lineClass;
        string lineDatasetPath = System.IO.Path.Combine(lineDataset.Workspace.PathName, lineDataset.Name);

        ExtractByMask extractTool = new ExtractByMask();
        RasterToPolygon toPolygonTool = new RasterToPolygon();
        Intersect intersectTool = new Intersect();
        Copy copyTool = new Copy();
        TabulateIntersection tabulateTool = new TabulateIntersection();

        OnProgress(new ProgressEventArgs(0, 1, "Extracting raster cell values..."), true);

        extractTool.in_raster = _raster;
        extractTool.in_mask_data = _lineClass;
        extractTool.out_raster = "extracted_raster";

        object result = GeoprocessingUtility.RunGpTool(gp, extractTool, OnProgress);
        Logger.Log(string.Format("Extracted raster: {0}", result));

        OnProgress(new ProgressEventArgs(0, 1, "Converting extraction to polygons..."), true);

        toPolygonTool.in_raster = result;
        toPolygonTool.out_polygon_features = "extracted_polygons";
        toPolygonTool.raster_field = _rasterFieldName;
        toPolygonTool.simplify = "NO_SIMPLIFY";

        result = GeoprocessingUtility.RunGpTool(gp, toPolygonTool, OnProgress);
        Logger.Log(string.Format("Extracted polygons: {0}", result));

        string currentWorkspace = gp.GetEnvironmentValue("workspace").ToString();
        if (lineDataset.Workspace.PathName != currentWorkspace)
        {
          OnProgress(new ProgressEventArgs(0, 1, "Copying polylines..."), true);

          copyTool.in_data = lineDatasetPath;
          copyTool.out_data = string.Format("{0}\\{1}", currentWorkspace, lineDataset.Name);
          object copyResult = GeoprocessingUtility.RunGpTool(gp, copyTool, OnProgress);
          Logger.Log(string.Format("Copied polylines: {0}", copyResult));
        }

        OnProgress(new ProgressEventArgs(0, 1, "Intersecting polylines with polygons..."), true);
        
        string class1 = lineDataset.Name;
        string class2 = System.IO.Path.GetFileName((string)result);
        intersectTool.in_features = string.Format("{0};{1};", class1, class2);
        intersectTool.out_feature_class =
          System.IO.Path.Combine(
          Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"temp\intersected_lines.shp");


        result = GeoprocessingUtility.RunGpTool(gp, intersectTool, OnProgress);
        Logger.Log(string.Format("Intersected polylines: {0}", result));

        gp.SetEnvironmentValue("workspace", currentWorkspace);

        OnProgress(new ProgressEventArgs(0, 1, "Tabulating intersection..."), true);

        tabulateTool.in_zone_features = lineDatasetPath;
        tabulateTool.zone_fields = _idFieldName;
        tabulateTool.in_class_features = result;
        tabulateTool.class_fields = _ValueFieldName;
        tabulateTool.out_table = "intersect_tabulation";

        result = GeoprocessingUtility.RunGpTool(gp, tabulateTool, OnProgress);
        Logger.Log(string.Format("Tabulated results: {0}", result));

        OnProgress(new ProgressEventArgs(0, 1, "Returning intersection tabulation..."), true);

        string tablePath = System.IO.Path.GetDirectoryName((string)result);
        string tableName = System.IO.Path.GetFileName((string)result);

        IWorkspaceFactory wsf = new FileGDBWorkspaceFactoryClass();
        IWorkspace ws = wsf.OpenFromFile(tablePath, 0);
        tabulation = ((IFeatureWorkspace)ws).OpenTable(tableName);
      }
      finally
      {
        GeoprocessingUtility.ResetGeoprocessor();
      }

      HashSet<object> values = _GetUniqueValues(tabulation);
      Dictionary<object, string> fieldLookup = _AddOutputFields(tabulation, values);
      _SummarizeTable(tabulation, fieldLookup);

      _LogIntersectEnd();
    }

    private void _LogIntersectStart()
    {
      Logger.Log("---------------------------------------");
      Logger.Log(string.Format("Polyline-Raster Intersect initiated by {0}", Environment.UserName));
      Logger.Log(string.Format("INPUTS:"));
      Logger.Log(string.Format("Category Raster: {0}", GeoprocessingUtility.GetRasterPath(this.CategoryRaster)));
      Logger.Log(string.Format("Raster Field: {0}", this.RasterField));
      Logger.Log(string.Format("Polyline Feature Class: {0}", GeoprocessingUtility.GetFeatureClassPath(this.PolylineClass)));
      Logger.Log(string.Format("Polyline Id Field: {0}", this.PolylineIdField));
      Logger.Log("---------------------------------------");
    }

    private void _LogIntersectEnd()
    {
      Logger.Log("---------------------------------------");
      Logger.Log(string.Format("Polyline-Raster Intersect completed."));
      Logger.Log(string.Format("OUTPUTS:"));
      Logger.Log(string.Format("Output Field Prefix: {0}", this.OutputFieldPrefix));
      Logger.Log("---------------------------------------");
    }

    private HashSet<object> _GetUniqueValues(ITable tabulation)
    {
      HashSet<object> values = new HashSet<object>();

      // Add field for each unique value
      int valueField = tabulation.FindField(_ValueFieldName);
      ICursor cursor = tabulation.Search(null, false);
      try
      {
        IRow row = cursor.NextRow();
        while (row != null)
        {
          try
          {
            object value = row.get_Value(valueField);
            values.Add(value);
          }
          finally
          {
            ComUtility.ReleaseComObject(row);
          }

          row = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      return values;
    }

    private Dictionary<object, string> _AddOutputFields(ITable tabulation, HashSet<object> values)
    {
      Dictionary<object, string> fieldLookup = new Dictionary<object, string>();

      if (values.Count > MAX_FIELDS)
        throw new Exception("There are too many unique values to add a field for each to the output table.");

      foreach(object value in values)
      {
        string fieldName = _GetFieldNameForValue(value);
        fieldLookup.Add(value, fieldName);
      }

      OnProgress(new ProgressEventArgs(0, 1, "Adding output fields..."), true);

      List<string> fieldNames = new List<string>(fieldLookup.Values);
      fieldNames.Sort();
      foreach (string fieldName in fieldNames)
      {
        if (_lineClass.FindField(fieldName) == -1)
          FeatureClassUtility.AddField(ref _lineClass, fieldName, esriFieldType.esriFieldTypeDouble, _useGeoprocessor);
      }

      // Add dominant value field
      IField valueField = tabulation.Fields.get_Field(tabulation.FindField(_ValueFieldName));
      _dominantFieldName = _GetDominantValueFieldName();
      if (_lineClass.FindField(_dominantFieldName) == -1)
      {
        if (valueField.Type == esriFieldType.esriFieldTypeString)
          FeatureClassUtility.AddField(ref _lineClass, _dominantFieldName, valueField.Type, valueField.Length, _useGeoprocessor);
        else
          FeatureClassUtility.AddField(ref _lineClass, _dominantFieldName, valueField.Type, _useGeoprocessor);
      }

      return fieldLookup;
    }

    private void _SummarizeTable(ITable tabulation, Dictionary<object, string> fieldLookup)
    {
      OnProgress(new ProgressEventArgs(0, 1, "Summarizing results...", true), true);

      int progress = 0;
      int total = tabulation.RowCount(null);
      int increment = _CalcIncrement(total);

      int idField = tabulation.FindField(_idFieldName);
      int valueField = tabulation.FindField(_ValueFieldName);
      int percentField = tabulation.FindField("PERCENTAGE");

      Dictionary<int, Dictionary<object, object>> crossTabLookup = new Dictionary<int, Dictionary<object, object>>();

      ICursor tabCursor = tabulation.Search(null, false);
      try
      {
        IRow tabRow = tabCursor.NextRow();
        while (tabRow != null)
        {
          try
          {
            int id = (int)tabRow.get_Value(idField);
            object dbValue = tabRow.get_Value(valueField);
            object percent = tabRow.get_Value(percentField);

            if (!crossTabLookup.ContainsKey(id))
              crossTabLookup.Add(id, new Dictionary<object, object>());

            crossTabLookup[id][dbValue] = percent;

            if (++progress % increment == 0)
            {
              ProgressEventArgs eventArgs = new ProgressEventArgs(progress, total, "Summarizing results...", true);
              OnProgress(eventArgs);
              if (eventArgs.Cancel)
                throw new UserCanceledException();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(tabRow);
          }
          tabRow = tabCursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(tabCursor);
      }

      OnProgress(new ProgressEventArgs(0, 1, "Saving results...", true), true);

      progress = 0;
      total = _lineClass.FeatureCount(null);
      increment = _CalcIncrement(total);

      idField = _lineClass.FindField(_idFieldName);
      int dominantField = _lineClass.FindField(_dominantFieldName);

      IFeatureCursor lineCursor = _lineClass.Update(null, false);
      try
      {
        IFeature line = lineCursor.NextFeature();
        while (line != null)
        {
          try
          {
            // Clear all field values
            foreach (string outputField in fieldLookup.Values)
            {
              line.set_Value(line.Fields.FindField(outputField), 0.0);
            }

            int id = (int)line.get_Value(idField);
            if (crossTabLookup.ContainsKey(id))
            {
              Dictionary<object, object> tabulationLookup = crossTabLookup[id];
              object dominant = DBNull.Value;
              double maxPercent = double.MinValue;
              double totalPercent = 0;
              foreach (object value in tabulationLookup.Keys)
              {
                line.set_Value(line.Fields.FindField(fieldLookup[value]), tabulationLookup[value]);
                if (tabulationLookup[value] != DBNull.Value)
                {
                  double percent = (double)tabulationLookup[value];
                  if (percent > maxPercent)
                  {
                    maxPercent = percent;
                    dominant = value;                    
                  }
                  totalPercent += percent;
                }
              }
              if (100 - totalPercent > maxPercent)
                dominant = DBNull.Value;

              line.set_Value(dominantField, dominant);
            }

            lineCursor.UpdateFeature(line);

            if (++progress % increment == 0)
            {
              ProgressEventArgs eventArgs = new ProgressEventArgs(progress, total, "Saving results...", true);
              OnProgress(eventArgs);
              if (eventArgs.Cancel)
                throw new UserCanceledException();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(line);
          }
          line = lineCursor.NextFeature();
        }
        lineCursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(lineCursor);
      }

      OnProgress(new ProgressEventArgs(1, 1, "Polyline-raster tabulation saved."), true);
    }


    private string _GetFieldNameForValue(object value)
    {
      bool usingGridCode = _ValueFieldName == GRIDCODE_FIELD;
      string valueAsString = value.ToString();
      if (usingGridCode)
        valueAsString = GRIDCODE_FIELD + "_" + valueAsString;
      string outField = string.IsNullOrEmpty(_outFieldPrefix) ? valueAsString : _outFieldPrefix + "_" + valueAsString;

      return FeatureClassUtility.ValidateField(_lineClass, outField);
    }

    private string _GetDominantValueFieldName()
    {
      string domFieldName = string.IsNullOrEmpty(_outFieldPrefix) ? "Dominant" : _outFieldPrefix + "_Dominant";
      return FeatureClassUtility.ValidateField(_lineClass, domFieldName);
    }

    private string _ValueFieldName
    {
      get
      {
        //TODO: Note that "gridcode" is used when saving value to GDB raster. May be "grid_code" in other situations... needs to be checked.
        return _rasterFieldName.ToLowerInvariant() == "value" ? GRIDCODE_FIELD : _rasterFieldName;
      }
    }

    private int _CalcIncrement(int total)
    {
      int increment = total / 1000;
      if (increment == 0)
        increment = 1;
      return increment;
    }

    protected virtual void OnProgress(ProgressEventArgs eventArgs, bool log = false)
    {
      if (Progress != null)
      {
        Progress(this, eventArgs);
      }
      if (log)
      {
        Logger.Log(eventArgs.Message);
      }
    }
  }
}
